Merge "Change name of main page in Sardinian (sc)"
[lhc/web/wiklou.git] / tests / phpunit / includes / libs / objectcache / BagOStuffTest.php
1 <?php
2
3 use Wikimedia\ScopedCallback;
4
5 /**
6 * @author Matthias Mullie <mmullie@wikimedia.org>
7 * @group BagOStuff
8 */
9 class BagOStuffTest extends MediaWikiTestCase {
10 /** @var BagOStuff */
11 private $cache;
12
13 const TEST_KEY = 'test';
14
15 protected function setUp() {
16 parent::setUp();
17
18 // type defined through parameter
19 if ( $this->getCliArg( 'use-bagostuff' ) !== null ) {
20 $name = $this->getCliArg( 'use-bagostuff' );
21
22 $this->cache = ObjectCache::newFromId( $name );
23 } else {
24 // no type defined - use simple hash
25 $this->cache = new HashBagOStuff;
26 }
27
28 $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) );
29 $this->cache->delete( $this->cache->makeKey( self::TEST_KEY ) . ':lock' );
30 }
31
32 /**
33 * @covers BagOStuff::makeGlobalKey
34 * @covers BagOStuff::makeKeyInternal
35 */
36 public function testMakeKey() {
37 $cache = ObjectCache::newFromId( 'hash' );
38
39 $localKey = $cache->makeKey( 'first', 'second', 'third' );
40 $globalKey = $cache->makeGlobalKey( 'first', 'second', 'third' );
41
42 $this->assertStringMatchesFormat(
43 '%Sfirst%Ssecond%Sthird%S',
44 $localKey,
45 'Local key interpolates parameters'
46 );
47
48 $this->assertStringMatchesFormat(
49 'global%Sfirst%Ssecond%Sthird%S',
50 $globalKey,
51 'Global key interpolates parameters and contains global prefix'
52 );
53
54 $this->assertNotEquals(
55 $localKey,
56 $globalKey,
57 'Local key and global key with same parameters should not be equal'
58 );
59
60 $this->assertNotEquals(
61 $cache->makeKeyInternal( 'prefix', [ 'a', 'bc:', 'de' ] ),
62 $cache->makeKeyInternal( 'prefix', [ 'a', 'bc', ':de' ] )
63 );
64 }
65
66 /**
67 * @covers BagOStuff::merge
68 * @covers BagOStuff::mergeViaCas
69 */
70 public function testMerge() {
71 $key = $this->cache->makeKey( self::TEST_KEY );
72
73 $calls = 0;
74 $casRace = false; // emulate a race
75 $callback = function ( BagOStuff $cache, $key, $oldVal ) use ( &$calls, &$casRace ) {
76 ++$calls;
77 if ( $casRace ) {
78 // Uses CAS instead?
79 $cache->set( $key, 'conflict', 5 );
80 }
81
82 return ( $oldVal === false ) ? 'merged' : $oldVal . 'merged';
83 };
84
85 // merge on non-existing value
86 $merged = $this->cache->merge( $key, $callback, 5 );
87 $this->assertTrue( $merged );
88 $this->assertEquals( 'merged', $this->cache->get( $key ) );
89
90 // merge on existing value
91 $merged = $this->cache->merge( $key, $callback, 5 );
92 $this->assertTrue( $merged );
93 $this->assertEquals( 'mergedmerged', $this->cache->get( $key ) );
94
95 $calls = 0;
96 $casRace = true;
97 $this->assertFalse(
98 $this->cache->merge( $key, $callback, 5, 1 ),
99 'Non-blocking merge (CAS)'
100 );
101 if ( $this->cache instanceof MultiWriteBagOStuff ) {
102 $wrapper = \Wikimedia\TestingAccessWrapper::newFromObject( $this->cache );
103 $n = count( $wrapper->caches );
104 } else {
105 $n = 1;
106 }
107 $this->assertEquals( $n, $calls );
108 }
109
110 /**
111 * @covers BagOStuff::merge
112 * @dataProvider provideTestMerge_fork
113 */
114 public function testMerge_fork( $exists, $childWins, $resCAS ) {
115 $key = $this->cache->makeKey( self::TEST_KEY );
116 $pCallback = function ( BagOStuff $cache, $key, $oldVal ) {
117 return ( $oldVal === false ) ? 'init-parent' : $oldVal . '-merged-parent';
118 };
119 $cCallback = function ( BagOStuff $cache, $key, $oldVal ) {
120 return ( $oldVal === false ) ? 'init-child' : $oldVal . '-merged-child';
121 };
122
123 if ( $exists ) {
124 $this->cache->set( $key, 'x', 5 );
125 }
126
127 /*
128 * Test concurrent merges by forking this process, if:
129 * - not manually called with --use-bagostuff
130 * - pcntl_fork is supported by the system
131 * - cache type will correctly support calls over forks
132 */
133 $fork = (bool)$this->getCliArg( 'use-bagostuff' );
134 $fork &= function_exists( 'pcntl_fork' );
135 $fork &= !$this->cache instanceof HashBagOStuff;
136 $fork &= !$this->cache instanceof EmptyBagOStuff;
137 $fork &= !$this->cache instanceof MultiWriteBagOStuff;
138 if ( $fork ) {
139 $pid = null;
140 // Function to start merge(), run another merge() midway through, then finish
141 $func = function ( $cache, $key, $cur ) use ( $pCallback, $cCallback, &$pid ) {
142 $pid = pcntl_fork();
143 if ( $pid == -1 ) {
144 return false;
145 } elseif ( $pid ) {
146 pcntl_wait( $status );
147
148 return $pCallback( $cache, $key, $cur );
149 } else {
150 $this->cache->merge( $key, $cCallback, 0, 1 );
151 // Bail out of the outer merge() in the child process since it does not
152 // need to attempt to write anything. Success is checked by the parent.
153 parent::tearDown(); // avoid phpunit notices
154 exit;
155 }
156 };
157
158 // attempt a merge - this should fail
159 $merged = $this->cache->merge( $key, $func, 0, 1 );
160
161 if ( $pid == -1 ) {
162 return; // can't fork, ignore this test...
163 }
164
165 // merge has failed because child process was merging (and we only attempted once)
166 $this->assertEquals( !$childWins, $merged );
167 $this->assertEquals( $this->cache->get( $key ), $resCAS );
168 } else {
169 $this->markTestSkipped( 'No pcntl methods available' );
170 }
171 }
172
173 function provideTestMerge_fork() {
174 return [
175 // (already exists, child wins CAS, result of CAS)
176 [ false, true, 'init-child' ],
177 [ true, true, 'x-merged-child' ]
178 ];
179 }
180
181 /**
182 * @covers BagOStuff::changeTTL
183 */
184 public function testChangeTTL() {
185 $key = $this->cache->makeKey( self::TEST_KEY );
186 $value = 'meow';
187
188 $this->cache->add( $key, $value, 5 );
189 $this->assertTrue( $this->cache->changeTTL( $key, 5 ) );
190 $this->assertEquals( $this->cache->get( $key ), $value );
191 $this->cache->delete( $key );
192 $this->assertFalse( $this->cache->changeTTL( $key, 5 ) );
193 }
194
195 /**
196 * @covers BagOStuff::add
197 */
198 public function testAdd() {
199 $key = $this->cache->makeKey( self::TEST_KEY );
200 $this->assertTrue( $this->cache->add( $key, 'test', 5 ) );
201 }
202
203 /**
204 * @covers BagOStuff::get
205 */
206 public function testGet() {
207 $value = [ 'this' => 'is', 'a' => 'test' ];
208
209 $key = $this->cache->makeKey( self::TEST_KEY );
210 $this->cache->add( $key, $value, 5 );
211 $this->assertEquals( $this->cache->get( $key ), $value );
212 }
213
214 /**
215 * @covers BagOStuff::get
216 * @covers BagOStuff::set
217 * @covers BagOStuff::getWithSetCallback
218 */
219 public function testGetWithSetCallback() {
220 $key = $this->cache->makeKey( self::TEST_KEY );
221 $value = $this->cache->getWithSetCallback(
222 $key,
223 30,
224 function () {
225 return 'hello kitty';
226 }
227 );
228
229 $this->assertEquals( 'hello kitty', $value );
230 $this->assertEquals( $value, $this->cache->get( $key ) );
231 }
232
233 /**
234 * @covers BagOStuff::incr
235 */
236 public function testIncr() {
237 $key = $this->cache->makeKey( self::TEST_KEY );
238 $this->cache->add( $key, 0, 5 );
239 $this->cache->incr( $key );
240 $expectedValue = 1;
241 $actualValue = $this->cache->get( $key );
242 $this->assertEquals( $expectedValue, $actualValue, 'Value should be 1 after incrementing' );
243 }
244
245 /**
246 * @covers BagOStuff::incrWithInit
247 */
248 public function testIncrWithInit() {
249 $key = $this->cache->makeKey( self::TEST_KEY );
250 $val = $this->cache->incrWithInit( $key, 0, 1, 3 );
251 $this->assertEquals( 3, $val, "Correct init value" );
252
253 $val = $this->cache->incrWithInit( $key, 0, 1, 3 );
254 $this->assertEquals( 4, $val, "Correct init value" );
255 }
256
257 /**
258 * @covers BagOStuff::getMulti
259 */
260 public function testGetMulti() {
261 $value1 = [ 'this' => 'is', 'a' => 'test' ];
262 $value2 = [ 'this' => 'is', 'another' => 'test' ];
263 $value3 = [ 'testing a key that may be encoded when sent to cache backend' ];
264 $value4 = [ 'another test where chars in key will be encoded' ];
265
266 $key1 = $this->cache->makeKey( 'test-1' );
267 $key2 = $this->cache->makeKey( 'test-2' );
268 // internally, MemcachedBagOStuffs will encode to will-%25-encode
269 $key3 = $this->cache->makeKey( 'will-%-encode' );
270 $key4 = $this->cache->makeKey(
271 'flowdb:flow_ref:wiki:by-source:v3:Parser\'s_"broken"_+_(page)_&_grill:testwiki:1:4.7'
272 );
273
274 // cleanup
275 $this->cache->delete( $key1 );
276 $this->cache->delete( $key2 );
277 $this->cache->delete( $key3 );
278 $this->cache->delete( $key4 );
279
280 $this->cache->add( $key1, $value1, 5 );
281 $this->cache->add( $key2, $value2, 5 );
282 $this->cache->add( $key3, $value3, 5 );
283 $this->cache->add( $key4, $value4, 5 );
284
285 $this->assertEquals(
286 [ $key1 => $value1, $key2 => $value2, $key3 => $value3, $key4 => $value4 ],
287 $this->cache->getMulti( [ $key1, $key2, $key3, $key4 ] )
288 );
289
290 // cleanup
291 $this->cache->delete( $key1 );
292 $this->cache->delete( $key2 );
293 $this->cache->delete( $key3 );
294 $this->cache->delete( $key4 );
295 }
296
297 /**
298 * @covers BagOStuff::setMulti
299 * @covers BagOStuff::deleteMulti
300 */
301 public function testSetDeleteMulti() {
302 $map = [
303 $this->cache->makeKey( 'test-1' ) => 'Siberian',
304 $this->cache->makeKey( 'test-2' ) => [ 'Huskies' ],
305 $this->cache->makeKey( 'test-3' ) => [ 'are' => 'the' ],
306 $this->cache->makeKey( 'test-4' ) => (object)[ 'greatest' => 'animal' ],
307 $this->cache->makeKey( 'test-5' ) => 4,
308 $this->cache->makeKey( 'test-6' ) => 'ever'
309 ];
310
311 $this->cache->setMulti( $map, 5 );
312 $this->assertEquals(
313 $map,
314 $this->cache->getMulti( array_keys( $map ) )
315 );
316
317 $this->assertTrue( $this->cache->deleteMulti( array_keys( $map ), 5 ) );
318
319 $this->assertEquals(
320 [],
321 $this->cache->getMulti( array_keys( $map ) )
322 );
323 }
324
325 /**
326 * @covers BagOStuff::getScopedLock
327 */
328 public function testGetScopedLock() {
329 $key = $this->cache->makeKey( self::TEST_KEY );
330 $value1 = $this->cache->getScopedLock( $key, 0 );
331 $value2 = $this->cache->getScopedLock( $key, 0 );
332
333 $this->assertType( ScopedCallback::class, $value1, 'First call returned lock' );
334 $this->assertNull( $value2, 'Duplicate call returned no lock' );
335
336 unset( $value1 );
337
338 $value3 = $this->cache->getScopedLock( $key, 0 );
339 $this->assertType( ScopedCallback::class, $value3, 'Lock returned callback after release' );
340 unset( $value3 );
341
342 $value1 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' );
343 $value2 = $this->cache->getScopedLock( $key, 0, 5, 'reentry' );
344
345 $this->assertType( ScopedCallback::class, $value1, 'First reentrant call returned lock' );
346 $this->assertType( ScopedCallback::class, $value1, 'Second reentrant call returned lock' );
347 }
348
349 /**
350 * @covers BagOStuff::__construct
351 * @covers BagOStuff::trackDuplicateKeys
352 */
353 public function testReportDupes() {
354 $logger = $this->createMock( Psr\Log\NullLogger::class );
355 $logger->expects( $this->once() )
356 ->method( 'warning' )
357 ->with( 'Duplicate get(): "{key}" fetched {count} times', [
358 'key' => 'foo',
359 'count' => 2,
360 ] );
361
362 $cache = new HashBagOStuff( [
363 'reportDupes' => true,
364 'asyncHandler' => 'DeferredUpdates::addCallableUpdate',
365 'logger' => $logger,
366 ] );
367 $cache->get( 'foo' );
368 $cache->get( 'bar' );
369 $cache->get( 'foo' );
370
371 DeferredUpdates::doUpdates();
372 }
373
374 /**
375 * @covers BagOStuff::lock()
376 * @covers BagOStuff::unlock()
377 */
378 public function testLocking() {
379 $key = 'test';
380 $this->assertTrue( $this->cache->lock( $key ) );
381 $this->assertFalse( $this->cache->lock( $key ) );
382 $this->assertTrue( $this->cache->unlock( $key ) );
383
384 $key2 = 'test2';
385 $this->assertTrue( $this->cache->lock( $key2, 5, 5, 'rclass' ) );
386 $this->assertTrue( $this->cache->lock( $key2, 5, 5, 'rclass' ) );
387 $this->assertTrue( $this->cache->unlock( $key2 ) );
388 $this->assertTrue( $this->cache->unlock( $key2 ) );
389 }
390 }